<template lang="pug">
  .vue-waterfall-ex-container(
    ref="outerEl"
    :style="containerStyle"
  )
    .vue-waterfall-ex-scroll(ref="scrollEl")
      slot(name="waterfall-head")
      .vue-waterfall-ex(:style="waterfallExStyle")
        .vue-waterfall-ex-img-box(
          v-for="(v,i) in displayItems"
          :key="v.id+'_'+i"
          :class="[{__err__: v._error}]"
          :style="getComputedStyle(v)"
          ref="imgBoxEls"
        )
          slot(:data="v")
            .vue-waterfall-ex-img-wraper(v-if="v[srcKey]")
              img(:src="v[srcKey]")
        .vue-waterfall-ex-over(v-if="over",ref="over")
          slot(name="waterfall-over") 被你看光了
</template>

<script lang="ts">
import { Component, Vue, Prop, Watch, Ref, Emit } from "vue-property-decorator";

import Throttle from "lodash-decorators/throttle";
import Bind from "lodash-decorators/bind";
import SectionManager from "./SectionManager";
import SmoothScrolling from "./SmoothScrolling";

interface WidthAndHeightObj {
  width: string;
  height: string;
}
interface DisplayItemObj {
  itemIndex: number;
  key: number;
  [key: string]: string | number;
}
interface ImgsObj {
  _x: number;
  _y: number;
  _width: number;
  _height: number;
  [key: string]: string | number;
}

let loadedCount: number;
let sectionManager: SectionManager | null = null;
let smoothScrolling: SmoothScrolling | null = null;

@Component({})
export default class VueWaterfallEx extends Vue {
  // ================= Props start =================
  @Prop({
    type: Number,
    default: 20
  })
  readonly gap!: number;

  @Prop({
    type: Number,
    validator(v: number): boolean {
      return v >= 0;
    }
  })
  readonly height?: number;

  @Prop({
    type: Array,
    required: true
  })
  readonly imgsArr!: ImgsObj[];

  @Prop({
    type: Number,
    default: 300
  })
  readonly imgWidth!: number;

  @Prop({
    type: Number,
    default: 0
  })
  readonly maxCols!: number;

  @Prop({
    type: Number,
    default: 0
  })
  readonly moreHeight!: number;

  @Prop({
    type: Number,
    default: 20
  })
  readonly reachBottomDistance!: number;

  @Prop({
    type: Number,
    default: 600
  })
  readonly sectionSize!: number;

  @Prop({
    type: String,
    default: "src"
  })
  readonly srcKey!: string;

  @Prop({
    type: Number,
    validator(v: number): boolean {
      return v >= 0;
    }
  })
  readonly width!: number;

  // ================= Props end =================
  // ================= Data start =================
  // 开始要排列的图片索引,首次为第二列的第一张图片，后续加载则为已经排列图片的下一个索引
  private beginIndex = 0;
  // 需要根据窗口宽度初始化
  private cols = 1;
  private colsHeightArr: number[] = [];
  private displayItems: DisplayItemObj[] = [];
  private imgsArrCopy: ImgsObj[] = [];
  private imgWidthCopy = 0;
  private isFirstLoad = true;
  private isLoading = false;
  private outerHeight = 0;
  private outerWidth = 0;
  // 结束waterfall加载
  private over = false;
  private resizeObserver?: ResizeObserver;
  private scrollPos: { x: number; y: number } = { x: 0, y: 0 };
  private totalHeight = 0;
  private totalWidth = 0;

  @Ref("outerEl") readonly outerEl!: HTMLDivElement;
  @Ref("scrollEl") readonly scrollEl!: HTMLDivElement;

  // ================= Data end =================
  // ================= Computed start =================
  private get containerStyle(): WidthAndHeightObj {
    return {
      height: this.height ? this.height + "px" : "100%",
      width: this.width ? this.width + "px" : "100%"
    };
  }

  private get waterfallExStyle(): WidthAndHeightObj {
    return {
      height: this.totalHeight + "px",
      width: this.totalWidth + "px"
    };
  }

  private get colWidth(): number {
    return this.imgWidthCopy + this.gap;
  }
  // ================= Computed end =================

  @Watch("imgsArr")
  private wImgsArr(newW: ImgsObj[]): void {
    this.isLoading = false;
    if (newW.length > 0) this.preload();
    else this.over = true;
  }

  private created(): void {
    this.$on("preloaded", () => {
      console.log("preloaded");
      this.isFirstLoad = true;
      this.waterfallNew();
      this.onCollectionChanged();
      this.mathTotalHeight();
      const x = this.totalWidth - this.scrollEl.clientWidth;
      const y = this.totalHeight - this.scrollEl.clientHeight;
      smoothScrolling &&
        smoothScrolling.updateSize({
          x: x < 0 ? 0 : x,
          y: y < 0 ? 0 : y
        });
    });
  }

  private mounted(): void {
    this.outerWidth = this.outerEl.clientWidth;
    this.outerHeight = this.outerEl.clientHeight;
    // console.log(this.outerEl, this.outerWidth, this.outerHeight);

    smoothScrolling = new SmoothScrolling(this.scrollEl);
    smoothScrolling.on("scroll", this.onScroll);

    sectionManager = new SectionManager(this.sectionSize);
    this.calcColsAndImgW();
    this.preload();
    

    if (ResizeObserver) {
      this.resizeObserver = new ResizeObserver(this.onContainerResized);
      this.resizeObserver.observe(this.outerEl);
    } else {
      this.outerEl.addEventListener("resize", this.onContainerResized);
    }
  }

  private beforeDestroy(): void {
    if (ResizeObserver) {
      this.resizeObserver && this.resizeObserver.disconnect();
    } else {
      this.outerEl.removeEventListener("resize", this.onContainerResized);
    }
  }
  // ================= Emit start =================
  @Emit("preloaded")
  private ePreloaded() {
    return;
  }

  @Emit("scrollReachBottom")
  private eScrollReachBottom() {
    return;
  }
  // ================= Emit end =================
  // ================= Method start =================
  private calcColsAndImgW(): void {
    if (!this.outerEl) return;
    const colWidth: number = this.imgWidth + this.gap;
    const w: number = this.outerEl.clientWidth + this.gap;
    let cols: number = Math.floor(w / colWidth);
    // 当剩余空间大于imgWidth一半时，cols加1
    cols += w % colWidth > this.imgWidth / 2 ? 1 : 0;
    cols = cols === 0 ? 1 : cols;
    const imgWidthCopy: number = Math.floor(w / cols - this.gap);
    if (this.maxCols > 0 && cols > this.maxCols) {
      this.cols = this.maxCols;
      this.imgWidthCopy =
        imgWidthCopy > this.imgWidth ? imgWidthCopy : this.imgWidth;
    } else {
      this.imgWidthCopy = imgWidthCopy;
      this.cols = cols;
    }
  }

  private flushDisplayItems(rect: {
    x: number;
    y: number;
    width: number;
    height: number;
  }) {
    const { x, y, width, height } = rect;
    const displayItems: DisplayItemObj[] = [];
    const indices: number[] = sectionManager 
      ? sectionManager.getCellIndices({
        _height: height,
        _width: width,
        _x: x,
        _y: y
      }) : [];
    for (let i = 0; i < indices.length; i++) {
      const itemIndex: number = indices[i];
      displayItems.push(
        Object.freeze({
          itemIndex,
          key: displayItems.length,
          ...this.imgsArrCopy[itemIndex]
        })
      );
    }
    if (window.requestAnimationFrame) {
      window.requestAnimationFrame(() => {
        this.displayItems = displayItems;
        this.$forceUpdate();
      });
    } else {
      this.displayItems = displayItems;
      this.$forceUpdate();
    }
  }

  private getComputedStyle(
    displayItem: DisplayItemObj
  ): {
    left: string;
    top: string;
    width: string;
    height: string;
  } {
    const { _width, _height, _x, _y } = displayItem;
    return {
      left: `${_x}px`,
      top: `${_y}px`,
      width: `${_width}px`,
      height: `${_height}px`
    };
  }

  private mathTotalHeight(): void {
    let h = 0;
    if (this.colsHeightArr.length > 0) {
      h = Math.max(...this.colsHeightArr) - this.gap;
    }
    this.totalHeight = h > 0 ? h : 0;
    this.totalWidth = this.cols * this.colWidth - this.gap;
  }

  private onCollectionChanged(): void {
    const collection = this.imgsArrCopy;
    for (let i: number = this.beginIndex; i < collection.length; i++) {
      if (!collection[i]) return;
      const { _x, _y, _width, _height } = collection[i];
      sectionManager && sectionManager.registerCell({
        index: i,
        cellMetadatum: {
          _x,
          _y,
          _width,
          _height
        }
      });
    }
    // 排列完之后，新增图片从这个索引开始预加载图片和排列
    this.beginIndex = this.imgsArrCopy.length;
    this.flushDisplayItems({
      ...this.scrollPos,
      width: this.scrollEl.clientWidth,
      height: this.scrollEl.clientHeight
    });
  }

  @Bind()
  @Throttle(250)
  private onContainerResized(): void {
    if (!this.outerEl) return; 
    
    // 竖版瀑布流
    
    if (this.outerEl.clientWidth !== this.outerWidth) {
      // 宽度发生变化，重新排版
      this.outerWidth = this.outerEl.clientWidth;
      this.resetCollection();
    } else if ( this.outerEl.clientHeight !== this.outerHeight ) {
      // 仅高度发生变化，只需更新smoothScrolling，不必要重新排版
      this.outerHeight = this.outerEl.clientHeight;
      const x = this.totalWidth - this.scrollEl.clientWidth;
      const y = this.totalHeight - this.scrollEl.clientHeight;
      smoothScrolling &&
        smoothScrolling.updateSize({
          x: x < 0 ? 0 : x,
          y: y < 0 ? 0 : y
        });
    }
  }

  @Bind()
  @Throttle(250)
  private onScroll(opt: MouseWheelEvent): void {
    if (this.totalHeight <= this.outerHeight) return;
    this.scrollPos.x = opt.x;
    this.scrollPos.y = opt.y < 0 ? Math.abs(opt.y) : 0;
    const scrollEl = this.scrollEl;
    this.flushDisplayItems({
      ...this.scrollPos,
      width: scrollEl.clientWidth,
      height: scrollEl.clientHeight
    });
    if (this.isLoading || this.over) return;
    const minHeight: number = Math.min(...this.colsHeightArr);
    if (
      this.scrollPos.y + scrollEl.offsetHeight >
      minHeight - this.reachBottomDistance
    ) {
      this.isLoading = true;
      // 滚动触底
      this.eScrollReachBottom();
    }
  }

  private preload(): void {
    let imgItem: ImgsObj;
    loadedCount = 0;
    if (this.imgsArr.length === 0) {
      // this.ePreloaded()
      return;
    }
    for (let i: number = loadedCount; i < this.imgsArr.length; i++) {
      imgItem = this.imgsArr[i];
      if (imgItem.width && imgItem.height) {
        loadedCount++;
        imgItem._width = this.imgWidthCopy;
        imgItem._height = Math.round(
          (this.imgWidthCopy * (imgItem.height as number)) /
            (imgItem.width as number)
        ) + this.moreHeight;
        if (loadedCount === this.imgsArr.length) {
          this.ePreloaded();
          return;
        }
      } else {
        console.log("没有宽高");
        //       const oImg = new Image();
        //       oImg.src = imgItem.src;
        //       oImg.onload = oImg.onerror = e => {
        //         this.loadedCount++;
        //         // 预加载图片，计算图片容器的高
        //         imgItem._height =
        //           e.type == "load"
        //             ? Math.round(this.imgWidthCopy / (oImg.width / oImg.height))
        //             : this.imgWidthCopy;
        //         if (e.type == "error") {
        //           imgItem._error = true;
        //           this.$emit("imgError", imgItem);
        //         }
        //         if (this.loadedCount == this.imgsArr.length) {
        //           this.$emit("preloaded");
        //         }
        //       };
      }
    }
  }

  private resetCollection(): void {
    this.totalWidth = 0;
    this.totalHeight = 0;
    this.beginIndex = 0;
    this.colsHeightArr = [];
    sectionManager = null;
    sectionManager = new SectionManager(this.sectionSize);
    this.calcColsAndImgW();
    this.waterfallAll();
    this.onCollectionChanged();
    this.mathTotalHeight();

    const x = this.totalWidth - this.scrollEl.clientWidth;
    const y = this.totalHeight - this.scrollEl.clientHeight;
    smoothScrolling &&
      smoothScrolling.updateSize({
        x: x < 0 ? 0 : x,
        y: y < 0 ? 0 : y
      });
  }

  private waterfallAll(): void {
    let top: number, left: number, height: number;
    const colWidth = this.colWidth;
    this.colsHeightArr = [];
    const newArr: ImgsObj[] = [];
    for (let i = 0; i < this.imgsArrCopy.length; i++) {
      this.imgsArrCopy[i]._width = this.imgWidthCopy;
      this.imgsArrCopy[i]._height = Math.round(
        (this.imgWidthCopy * (this.imgsArrCopy[i].height as number)) /
          (this.imgsArrCopy[i].width as number)
      ) + this.moreHeight;
      height = (this.imgsArrCopy[i]._height as number) + this.gap;
      if (newArr.length < this.cols) {
        this.colsHeightArr.push(height);
        top = 0;
        left = i * colWidth;
      } else {
        const minHeight: number = Math.min(...this.colsHeightArr);
        const minIndex: number = this.colsHeightArr.indexOf(minHeight);
        // 设置元素定位的位置
        top = minHeight;
        left = minIndex * colWidth;
        // 更新colsHeightArr
        this.colsHeightArr[minIndex] = minHeight + height;
      }
      this.imgsArrCopy[i]._y = top;
      this.imgsArrCopy[i]._x = left;
      newArr.push({
        ...this.imgsArrCopy[i]
      });
    }
    this.imgsArrCopy = newArr;
  }

  private waterfallNew(): void {
    if (this.imgsArr.length === 0) return;
    let top: number, left: number, height: number;
    const colWidth = this.colWidth;
    for (let i = 0; i < this.imgsArr.length; i++) {
      if (!this.imgsArr[i]) return;
      height = (this.imgsArr[i]._height as number) + this.gap;
      if (this.imgsArrCopy.length < this.cols) {
        this.colsHeightArr.push(height);
        top = 0;
        left = i * colWidth;
      } else {
        const minHeight: number = Math.min(...this.colsHeightArr);
        const minIndex: number = this.colsHeightArr.indexOf(minHeight);
        // 设置元素定位的位置
        top = minHeight;
        left = minIndex * colWidth;
        // 更新colsHeightArr
        this.colsHeightArr[minIndex] = minHeight + height;
      }
      this.imgsArr[i]._y = top;
      this.imgsArr[i]._x = left;
      this.imgsArrCopy.push({
        ...this.imgsArr[i]
      });
    }
  }
  // ================= Method end =================
}
</script>

<style lang="scss" scoped>
.vue-waterfall-ex-container {
  // width: 100%;
  // height: 100%;
  position: relative;

  .vue-waterfall-ex-scroll {
    position: relative;
    width: 100%;
    height: 100%;
    // overflow-x: hidden;
    // overflow-y: scroll;
    overflow: hidden;
    -webkit-overflow-scrolling: touch;
  }

  .vue-waterfall-ex {
    margin: 0 auto;
    position: relative;

    & > .vue-waterfall-ex-img-box {
      position: absolute;
    }

    .vue-waterfall-ex-img-wraper {
      position: absolute;
      z-index: 0;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;

      img {
        width: 100%;
        height: auto;
        display: block;
      }
    }

    .__err__ .vue-waterfall-ex-img-wraper {
      background-image: url();
      background-repeat: no-repeat;
      background-position: center;
      background-size: 50% 50%;

      img {
        display: none;
      }
    }
  }

  .vue-waterfall-ex-over {
    position: absolute;
    bottom: 0;
    left: 0;
    transform: translateY(100%);
    width: 100%;
    height: 50px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 12px;
    color: #aaa;
  }
}
</style>
